package com.nc.mem.serial.graph.asm;

import static org.objectweb.asm.Opcodes.AASTORE;
import static org.objectweb.asm.Opcodes.ACC_FINAL;
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ANEWARRAY;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.ASTORE;
import static org.objectweb.asm.Opcodes.BIPUSH;
import static org.objectweb.asm.Opcodes.CHECKCAST;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.GETFIELD;
import static org.objectweb.asm.Opcodes.GETSTATIC;
import static org.objectweb.asm.Opcodes.ICONST_0;
import static org.objectweb.asm.Opcodes.ICONST_1;
import static org.objectweb.asm.Opcodes.ICONST_2;
import static org.objectweb.asm.Opcodes.ICONST_3;
import static org.objectweb.asm.Opcodes.ICONST_4;
import static org.objectweb.asm.Opcodes.ICONST_5;
import static org.objectweb.asm.Opcodes.ILOAD;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.ISTORE;
import static org.objectweb.asm.Opcodes.NEW;
import static org.objectweb.asm.Opcodes.PUTFIELD;
import static org.objectweb.asm.Opcodes.PUTSTATIC;
import static org.objectweb.asm.Opcodes.RETURN;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.ByteBuffer;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.util.ASMifier;
import org.objectweb.asm.util.TraceClassVisitor;

import com.nc.mem.GraphClassLoader;
import com.nc.mem.PrivateAccessor;
import com.nc.mem.serial.Serializer;
import com.nc.mem.serial.graph.GraphSerializer;
import com.nc.mem.serial.graph.Representation;
import com.nc.mem.serial.graph.cust.Serializable;
import com.nc.mem.util.Utils;

class GraphClassAdapter extends ClassVisitor {

	/**
	 * {@link GraphSerializer#doWrite(ByteBuffer, Object)} Stack is:
	 * <ul>
	 * <li>0 => this</li>
	 * <li>1 => buffer</li>
	 * <li>2 => obj</li>
	 * <li>3 => (Cast to Type)obj</li>
	 * <li>4 => flags/accessor (if defined)</li>
	 * <li>5 => flags, if accessor</li>
	 * </ul>
	 * 
	 * @author cmuramoto
	 */
	class DoReadAdapter extends MethodVisitor {

		final String putObjectDesc = serializePrivate ? "(" + Type.getDescriptor(Object.class) + 'J' + Type.getDescriptor(Object.class) + ")" + 'V' : null;

		public DoReadAdapter(final MethodVisitor mv) {
			super(Opcodes.ASM4, mv);
		}

		@Override
		public void visitCode() {
			final MethodVisitor mv = this.mv;
			super.visitCode();

			final int b = 1;
			final int r = 2;
			final int o = 3;
			int fl = 4;
			final int a = 4;

			if (ipwf != null) {
				mv.visitFieldInsn(GETSTATIC, fullName, "$UPER_SER", gsType.getDescriptor());
				mv.visitVarInsn(ALOAD, b);
				mv.visitVarInsn(ALOAD, r);
				mv.visitMethodInsn(INVOKEVIRTUAL, gsType.getInternalName(), "doRead", "(Ljava/nio/ByteBuffer;Ljava/lang/Object;)V");
			}

			mv.visitVarInsn(ALOAD, r);
			mv.visitTypeInsn(CHECKCAST, rootType.getInternalName());
			mv.visitVarInsn(ASTORE, o);

			if (serializePrivate && PrivateAccessor.INSTANCE != null) {
				mv.visitFieldInsn(GETSTATIC, fullName, "_ACCESSOR", PVT_ACC_DESC);
				mv.visitVarInsn(ASTORE, a);
				fl++;
			}

			try {

				// primitives
				for (final FieldInfo fi : prims) {

					final String readMethName = MessageFormat.format("read{0}{1}", fi.typeName().substring(0, 1).toUpperCase(), fi.typeName().substring(1));

					final Method method = Representation.class.getDeclaredMethod(readMethName, ByteBuffer.class);

					if (fi.isPrivate() || fi.isFinal()) {
						if (PrivateAccessor.INSTANCE != null) {
							// mv.visitFieldInsn(GETSTATIC, fullName,
							// "_ACCESSOR", PVT_ACC_DESC);
							mv.visitVarInsn(ALOAD, a);
							mv.visitVarInsn(ALOAD, o);
							mv.visitLdcInsn(fi.offset);
							mv.visitVarInsn(ALOAD, b);
							mv.visitMethodInsn(INVOKESTATIC, Representation.INTERNAL_NAME, readMethName, Type.getMethodDescriptor(method));

							String name = "put";

							final Type type = fi.type;
							name += Character.toUpperCase(type.getClassName().charAt(0)) + type.getClassName().substring(1);
							final String desc = "(" + Type.getDescriptor(Object.class) + Type.getDescriptor(long.class) + fi.desc() + ")" + 'V';

							mv.visitMethodInsn(INVOKEVIRTUAL, PVT_ACC_IN, name, desc);
						} else {

						}
					} else {
						mv.visitVarInsn(ALOAD, o);
						mv.visitVarInsn(ALOAD, b);
						mv.visitMethodInsn(INVOKESTATIC, Representation.INTERNAL_NAME, readMethName, Type.getMethodDescriptor(method));
						mv.visitFieldInsn(PUTFIELD, fi.owner(), fi.name(), fi.desc());
					}
				}

				final int size = refs.size();

				if (size > 0) {
					final List<List<FieldInfo>> pLists = Utils.partition(refs, 31);

					final int[] iConsts = { ICONST_0, ICONST_1, ICONST_2, ICONST_3, ICONST_4, ICONST_5 };

					for (final List<FieldInfo> list : pLists) {
						mv.visitVarInsn(ALOAD, b);
						mv.visitMethodInsn(INVOKESTATIC, Representation.INTERNAL_NAME, "readNullFlags", "(Ljava/nio/ByteBuffer;)I");
						mv.visitVarInsn(ISTORE, fl);

						final int sz = list.size();
						int j = 0;
						String readNested = null;
						String readNestedNonPoly = null;

						for (int i = 0; i < sz; i++) {
							final FieldInfo fi = list.get(i);
							final boolean pvt = fi.isPrivate();

							final Class<?> fieldType = fi.type();
							final boolean nonPoly = notPolyRefs.contains(fi);

							if (pvt) {
								mv.visitVarInsn(ALOAD, a);
							}
							mv.visitVarInsn(ALOAD, o);

							if (pvt) {
								mv.visitLdcInsn(fi.offset);
							}

							mv.visitVarInsn(ALOAD, b);

							if (fieldType == Boolean.class || fieldType == Character.class || Number.class.isAssignableFrom(fieldType)) {
								mv.visitVarInsn(ILOAD, fl);
								if (j < 6) {
									mv.visitInsn(iConsts[j]);
								} else {
									mv.visitIntInsn(BIPUSH, j);
								}
								final String name = "read" + fieldType.getSimpleName();
								final Method method = Representation.class.getDeclaredMethod(name, ByteBuffer.class, int.class, int.class);
								mv.visitMethodInsn(INVOKESTATIC, Representation.INTERNAL_NAME, name, Type.getMethodDescriptor(method));
								if (pvt) {
									mv.visitMethodInsn(INVOKEVIRTUAL, PVT_ACC_IN, "putObject", putObjectDesc);
								} else {
									// mv.visitTypeInsn(CHECKCAST, fi.internalName());
									mv.visitFieldInsn(PUTFIELD, fi.owner(), fi.name(), fi.desc());
								}
							} else if (fieldType.isEnum()) {
								mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(fieldType), "values", "()[" + Type.getDescriptor(fieldType));
								mv.visitVarInsn(ILOAD, fl);
								if (j < 6) {
									mv.visitInsn(iConsts[j]);
								} else {
									mv.visitIntInsn(BIPUSH, j);
								}
								final String name = "readEnum";
								final Method method = Representation.class.getDeclaredMethod(name, ByteBuffer.class, Enum[].class, int.class, int.class);
								mv.visitMethodInsn(INVOKESTATIC, Representation.INTERNAL_NAME, name, Type.getMethodDescriptor(method));
								if (pvt) {
									mv.visitMethodInsn(INVOKEVIRTUAL, PVT_ACC_IN, "putObject", putObjectDesc);
								} else {
									mv.visitTypeInsn(CHECKCAST, fi.internalName());
									mv.visitFieldInsn(PUTFIELD, fi.owner(), fi.name(), fi.desc());
								}
							} else {
								if (nonPoly) {
									if (fi.type.equals(rootType)) {
										mv.visitFieldInsn(GETSTATIC, fullName, "INSTANCE", fullDesc);
									} else {
										mv.visitFieldInsn(GETSTATIC, fullName, cachedNonPolyDescs.get(fi.type), gsType.getDescriptor());
									}
								}

								mv.visitVarInsn(ILOAD, fl);
								if (j < 6) {
									mv.visitInsn(iConsts[j]);
								} else {
									mv.visitIntInsn(BIPUSH, j);
								}

								if (nonPoly) {
									readNestedNonPoly = readNestedNonPoly == null ? Type.getMethodDescriptor(GraphSerializerFactory.class.getDeclaredMethod("readNested", ByteBuffer.class, GraphSerializer.class, int.class, int.class)) : readNestedNonPoly;
									mv.visitMethodInsn(INVOKESTATIC, gsfType.getInternalName(), "readNested", readNestedNonPoly);
								} else {
									readNested = readNested == null ? Type.getMethodDescriptor(GraphSerializerFactory.class.getDeclaredMethod("readNested", ByteBuffer.class, int.class, int.class)) : readNested;
									mv.visitMethodInsn(INVOKESTATIC, gsfType.getInternalName(), "readNested", readNested);
								}

								if (pvt) {
									mv.visitMethodInsn(INVOKEVIRTUAL, PVT_ACC_IN, "putObject", putObjectDesc);
								} else {
									mv.visitTypeInsn(CHECKCAST, fi.internalName());
									mv.visitFieldInsn(PUTFIELD, fi.owner(), fi.name(), fi.desc());
								}
							}

							j++;
						}
					}
				}
			} catch (final Exception exc) {
				throw new RuntimeException(exc);
			}

			mv.visitInsn(RETURN);
			mv.visitMaxs(0, 0);
		}

	}

	/**
	 * {@link GraphSerializer#doWrite(ByteBuffer, Object)} Stack is:
	 * <ul>
	 * <li>0 => this</li>
	 * <li>1 => buffer</li>
	 * <li>2 => obj</li>
	 * <li>3 => (Cast to Type)obj</li>
	 * <li>4 => flags</li>
	 * </ul>
	 * 
	 * @author cmuramoto
	 */
	class DoWriteAdapter extends MethodVisitor {

		final String getObjectDesc = serializePrivate ? "(" + Type.getDescriptor(Object.class) + 'J' + ")" + Type.getDescriptor(Object.class) : null;

		public DoWriteAdapter(final MethodVisitor mv) {
			super(Opcodes.ASM4, mv);
		}

		private String descriptorOf(final String methodName) throws Exception {
			return Type.getMethodDescriptor(GraphSerializerFactory.class.getDeclaredMethod(methodName, ByteBuffer.class, GraphSerializer.class, Object.class, int.class, int.class));
		}

		private String mapDescriptorOf() throws SecurityException, NoSuchMethodException {
			return Type.getMethodDescriptor(GraphSerializerFactory.class.getDeclaredMethod("writeFixMap", ByteBuffer.class, GraphSerializer.class, Object.class, int.class, int.class, boolean.class, boolean.class));
		}

		private void pushObjectOntoStack(final MethodVisitor mv, final FieldInfo fi, final boolean needsCast, final int accIx, final int objIx) {
			if (fi.isPrivate()) {
				if (PrivateAccessor.INSTANCE != null) {
					// mv.visitFieldInsn(GETSTATIC, fullName, "_ACCESSOR",
					// PVT_ACC_DESC);
					mv.visitVarInsn(ALOAD, accIx);
					mv.visitVarInsn(ALOAD, objIx);
					mv.visitLdcInsn(fi.offset);
					mv.visitMethodInsn(INVOKEVIRTUAL, PVT_ACC_IN, "getObject", getObjectDesc);
					if (needsCast) {
						mv.visitTypeInsn(CHECKCAST, fi.internalName());
					}
				} else {

				}
			} else {
				mv.visitVarInsn(ALOAD, objIx);
				mv.visitFieldInsn(GETFIELD, rootType.getInternalName(), fi.name(), fi.desc());
			}

		}

		private void pushPrimitiveOntoStack(final MethodVisitor mv, final FieldInfo fi, final int accIx) {
			if (fi.isPrivate()) {
				if (PrivateAccessor.INSTANCE != null) {
					// mv.visitFieldInsn(GETSTATIC, fullName,
					// "_ACCESSOR",PVT_ACC_DESC);
					mv.visitVarInsn(ALOAD, accIx);
					mv.visitVarInsn(ALOAD, 3);
					mv.visitLdcInsn(fi.offset);
					String name = "get";

					final Type type = fi.type;
					name += Character.toUpperCase(type.getClassName().charAt(0)) + type.getClassName().substring(1);
					final String desc = "(" + Type.getDescriptor(Object.class) + Type.getDescriptor(long.class) + ")" + fi.desc();
					mv.visitMethodInsn(INVOKEVIRTUAL, PVT_ACC_IN, name, desc);
				} else {

				}
			} else {
				mv.visitVarInsn(ALOAD, 3);
				mv.visitFieldInsn(GETFIELD, fi.owner(), fi.name(), fi.desc());
			}

		}

		@Override
		public void visitCode() {
			final MethodVisitor mv = this.mv;
			mv.visitCode();

			final int b = 1;
			final int r = 2;
			final int o = 3;
			int fl = 4;
			final int a = 4;
			if (ipwf != null) {
				mv.visitFieldInsn(GETSTATIC, fullName, "$UPER_SER", gsType.getDescriptor());
				mv.visitVarInsn(ALOAD, b);
				mv.visitVarInsn(ALOAD, 2);
				mv.visitMethodInsn(INVOKEVIRTUAL, gsType.getInternalName(), "doWrite", "(Ljava/nio/ByteBuffer;Ljava/lang/Object;)V");
			}

			mv.visitVarInsn(ALOAD, r);
			mv.visitTypeInsn(CHECKCAST, rootType.getInternalName());
			mv.visitVarInsn(ASTORE, o);
			if (serializePrivate && PrivateAccessor.INSTANCE != null) {
				mv.visitFieldInsn(GETSTATIC, fullName, "_ACCESSOR", PVT_ACC_DESC);
				mv.visitVarInsn(ASTORE, a);
				fl++;
			}

			// primitives
			for (final FieldInfo fi : prims) {
				mv.visitVarInsn(ALOAD, b);
				pushPrimitiveOntoStack(mv, fi, a);
				final String name = "write" + fi.typeName().substring(0, 1).toUpperCase() + fi.typeName().substring(1);
				try {
					final Method method = Representation.class.getDeclaredMethod(name, ByteBuffer.class, fi.type());
					mv.visitMethodInsn(INVOKESTATIC, Representation.INTERNAL_NAME, name, Type.getMethodDescriptor(method));
				} catch (final Exception exc) {
					exc.printStackTrace();
				}
			}

			// refs
			final int size = refs.size();

			if (size > 0) {
				final int[] iConsts = { ICONST_0, ICONST_1, ICONST_2, ICONST_3, ICONST_4, ICONST_5 };
				try {
					final Iterable<List<FieldInfo>> pLists = Utils.partition(refs, 31);
					final Method method = Representation.class.getDeclaredMethod("writeNullFlags", ByteBuffer.class, Object[].class);
					for (final List<FieldInfo> list : pLists) {
						final int sz = list.size();

						// load the buffer
						mv.visitVarInsn(ALOAD, b);

						// load an array to pass all values
						if (sz <= iConsts.length - 1) {
							mv.visitInsn(iConsts[sz]);
						} else {
							mv.visitIntInsn(BIPUSH, sz);
						}

						// load new Object[]
						mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
						mv.visitInsn(DUP);

						for (int i = 0; i < sz; i++) {
							final FieldInfo fi = list.get(i);

							// access array index
							if (i < 6) {
								mv.visitInsn(iConsts[i]);
							} else {
								mv.visitIntInsn(BIPUSH, i);
							}

							// place value on array index
							pushObjectOntoStack(mv, fi, false, a, o);
							mv.visitInsn(AASTORE);

							if (i == sz - 1) {
								// all loaded
								mv.visitMethodInsn(INVOKESTATIC, Representation.INTERNAL_NAME, "writeNullFlags", Type.getMethodDescriptor(method));
							} else {
								// place Object[] on top of the stack again
								mv.visitInsn(DUP);
							}
						}

						mv.visitVarInsn(ISTORE, fl);

						String writeNested = null;
						String writeNestedNonPoly = null;
						String writeFixCol = null;
						String writeFixColPayload = null;

						for (int i = 0; i < sz; i++) {
							final FieldInfo fi = list.get(i);
							final boolean nonPoly = notPolyRefs.contains(fi);
							final Class<?> fieldType = fi.type();

							mv.visitVarInsn(ALOAD, b);
							if (nonPoly) {
								if (fi.type.equals(rootType)) {
									mv.visitFieldInsn(GETSTATIC, fullName, "INSTANCE", fullDesc);
								} else {
									mv.visitFieldInsn(GETSTATIC, fullName, cachedNonPolyDescs.get(fi.type), gsType.getDescriptor());
								}
							}

							final boolean isWrittenInline = fieldType.isEnum() || fieldType == Boolean.class || fieldType == Character.class || Number.class.isAssignableFrom(fieldType);
							// place value on array index
							pushObjectOntoStack(mv, fi, isWrittenInline, a, o);

							mv.visitVarInsn(ILOAD, fl);

							if (i < 6) {
								mv.visitInsn(iConsts[i]);
							} else {
								mv.visitIntInsn(BIPUSH, i);
							}

							// If type is a Wrapper and the field type is not
							// number we inline the call to avoid overhead.
							if (isWrittenInline) {
								final String name = "write" + (fieldType.isEnum() ? "Enum" : fieldType.getSimpleName());
								final Method inlineWriteWrapper = Representation.class.getDeclaredMethod(name, ByteBuffer.class, fieldType.isEnum() ? Enum.class : fieldType, int.class, int.class);
								mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(Representation.class), name, Type.getMethodDescriptor(inlineWriteWrapper));
							} else {
								String targetDesc = null;
								String targetName = null;
								if (nonPoly) {
									if (fi.isCollection()) {
										if (fi.isOnlyPayloadCollection()) {
											targetName = "writeFixColPayload";
											targetDesc = writeFixColPayload = writeFixColPayload == null ? descriptorOf(targetName) : writeFixColPayload;
										} else {
											targetName = "writeFixCol";
											targetDesc = writeFixCol = writeFixCol == null ? descriptorOf(targetName) : writeFixCol;
										}
									} else if (fi.isMap()) {
										targetName = "writeFixMap";

										targetDesc = mapDescriptorOf();
										mv.visitInsn(fi.isOnlyPayloadMapKey() ? ICONST_1 : ICONST_0);
										mv.visitInsn(fi.isOnlyPayloadMapVal() ? ICONST_1 : ICONST_0);
									}

									if (targetDesc == null) {
										targetName = "writeNested";
										targetDesc = writeNestedNonPoly = writeNestedNonPoly == null ? descriptorOf("writeNested") : writeNestedNonPoly;
									}

									mv.visitMethodInsn(INVOKESTATIC, gsfType.getInternalName(), targetName, targetDesc);

								} else {
									writeNested = writeNested == null ? Type.getMethodDescriptor(GraphSerializerFactory.class.getDeclaredMethod("writeNested", ByteBuffer.class, Object.class, int.class, int.class)) : writeNested;
									mv.visitMethodInsn(INVOKESTATIC, gsfType.getInternalName(), "writeNested", writeNested);
								}
							}
						}
					}
				} catch (final Exception exc) {
					exc.printStackTrace();
				}
			}
			mv.visitInsn(RETURN);
			mv.visitMaxs(0, 0);
		}

	}

	public static File LOG_DIR;

	static {
		PVT_ACC_DESC = PrivateAccessor.INSTANCE == null ? null : Type.getDescriptor(PrivateAccessor.INSTANCE.getClass());
		PVT_ACC_IN = PrivateAccessor.INSTANCE == null ? null : Type.getInternalName(PrivateAccessor.INSTANCE.getClass());
	}

	private static final String PVT_ACC_DESC;

	private static final String PVT_ACC_IN;

	private static void save(final byte[] byteCode, final String clazz) {
		File f = LOG_DIR;

		String cn = clazz;

		if (cn.lastIndexOf('/') != -1) {
			cn = cn.substring(cn.lastIndexOf('/') + 1);
		}

		if (f == null) {
			f = new File(System.getProperty("user.home"));

			final String[] sub = new String[]{ "Work", "serialization", GraphSerializerFactory.class.getSimpleName() };

			for (final String subDir : sub) {
				f = new File(f, subDir);
			}

			if (!f.exists()) {
				f.mkdirs();
			}

			LOG_DIR = f;
		}

		if (!f.exists()) {
			f.mkdir();
		}

		f = new File(f, cn + "_ser.class");

		if (f.exists()) {
			f.delete();
		}

		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(f);
			fos.write(byteCode);
		} catch (final Exception exc) {

		} finally {
			if (fos != null) {
				try {
					fos.close();
				} catch (final Exception e) {
					e.printStackTrace();
				}
			}
		}
	}

	private Type gsfType = Type.getType(GraphSerializerFactory.class);

	private Type gsType = Type.getType(GraphSerializer.class);

	static int seq;
	/**
	 * Immediate parent with fields. In a hierarchy Object <- A <- B <- C, if B has no Serializable
	 * fields but A does, then A is the ipwf. In this case an extra block of code will be generated
	 * in order to delegate the serialization of the parent's fields to its corresponding
	 * Serializer. This is necessary because we can't generate correct code if the hierarchy spans
	 * multiple packages.
	 */
	Class<?> ipwf;
	/**
	 * The class we want to create a serializer for
	 */
	Type rootType;
	/**
	 * The name of the generated serializer
	 */
	private String fullName;

	/**
	 * The type descriptor of the generated serializer
	 */
	private String fullDesc;

	/**
	 * Primitive fields. Special code is emitted for them since there's no need for null checks.
	 */
	List<FieldInfo> prims;
	/**
	 * Reference fields: serialization is delegated to another serializer
	 */
	List<FieldInfo> refs;

	/**
	 * Non polymorphic references. The serializer can be resolved at class generation time, not at
	 * runtime. Plus, it will greatly optimize the serialization of collections.
	 */
	List<FieldInfo> notPolyRefs;

	/**
	 * Derived from @{@link Serializable}. If it resolves that any of
	 */
	boolean serializePrivate;

	/**
	 * 
	 */
	boolean hasAccessibleConstructor;

	private Map<Type, String> cachedNonPolyDescs;

	public GraphClassAdapter(final Class<?> rootType) {
		this(new ClassWriter(ClassWriter.COMPUTE_FRAMES), rootType);

	}

	public GraphClassAdapter(final ClassVisitor cv, final Class<?> rootType) {
		super(Opcodes.ASM4, cv);
		this.rootType = Type.getType(rootType);
		checkeAccessibleCtor(rootType);
	}

	private void checkeAccessibleCtor(final Class<?> type) {
		try {
			if (!Modifier.isPrivate(type.getDeclaredConstructor().getModifiers())) {
				hasAccessibleConstructor = true;
			}
		} catch (final Throwable err) {
			hasAccessibleConstructor = false;
		}
	}

	public GraphSerializer create(final ClassLoader cl, final boolean save, final OutputStream trace) throws IOException, InstantiationException, IllegalAccessException, IllegalArgumentException, SecurityException, NoSuchFieldException {
		InputStream stream = null;

		try {
			verifyNonPolyRefs();
			stream = GraphSerializer.class.getResourceAsStream(GraphSerializer.class.getSimpleName() + ".class");
			final ClassReader cr = new ClassReader(stream);

			cr.accept(this, 0);

			final byte[] byteCode = ((ClassWriter) cv).toByteArray();

			if (trace != null) {
				final ClassReader dumper = new ClassReader(byteCode.clone());
				dumper.accept(new TraceClassVisitor(null, new ASMifier(), new PrintWriter(trace)), ClassReader.SKIP_DEBUG);
			}

			if (save) {
				save(byteCode, rootType.getInternalName());
			}

			final Class<?> clazz = GraphClassLoader.defineClass(cl, byteCode);

			return (GraphSerializer) clazz.getDeclaredField("INSTANCE").get(null);
		} finally {
			if (stream != null) {
				stream.close();
			}
		}
	}

	void setAccessAsPrivate() {
		Iterable<FieldInfo> all = prims;
		all = all == null ? refs : Utils.concat(all, refs);
		all = all == null ? notPolyRefs : Utils.concat(all, notPolyRefs);

		if (all != null) {
			for (final FieldInfo fi : all) {
				fi.treatAsPrivate();
			}
		}
	}

	private void verifyNonPolyRefs() {
		final List<FieldInfo> nprs = notPolyRefs;
		if (nprs != null && !nprs.isEmpty()) {
			final HashMap<Type, String> map = new HashMap<Type, String>(2);
			for (final FieldInfo info : nprs) {
				final Type type = info.type;
				String desc = map.get(type);

				if (desc == null) {
					if (type.equals(rootType)) {
						desc = "INSTANCE";
					} else {
						desc = "$NON_POLY_" + info.typeName().replace('.', '_').replace("[", "");
						map.put(type, desc);
					}
				}
			}
			cachedNonPolyDescs = map;
		}
	}

	@Override
	public void visit(final int version, final int access, final String name, final String signature, final String superName, final String[] interfaces) {
		final String[] ifs = null;
		final String in = rootType.getInternalName();

		if (in.startsWith("java") || in.startsWith("sun")) {
			final int lastSlash = in.lastIndexOf('/');
			fullName = Type.getInternalName(GraphSerializer.class) + "__Of__" + in.substring(lastSlash + 1) + ++seq;
		} else {
			fullName = rootType.getInternalName() + "$GRAPH_SER";
		}

		fullDesc = "L" + fullName + ";";
		super.visit(version, ACC_PUBLIC + ACC_FINAL, fullName, signature, name, ifs);
	}

	@Override
	public void visitEnd() {
		super.visitEnd();

		// begin static initializer
		MethodVisitor mv;
		mv = cv.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
		mv.visitCode();

		/*
		 * Make me a singleton. This call must come before ANYTHING
		 */
		FieldVisitor fv = cv.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "INSTANCE", "L" + fullName + ";", null, null);
		fv.visitEnd();

		mv.visitTypeInsn(NEW, fullName);
		mv.visitInsn(DUP);
		mv.visitMethodInsn(INVOKESPECIAL, fullName, "<init>", "()V");
		mv.visitFieldInsn(PUTSTATIC, fullName, "INSTANCE", "L" + fullName + ";");

		if (ipwf != null || !notPolyRefs.isEmpty() || serializePrivate || !hasAccessibleConstructor) {

			if (serializePrivate || !hasAccessibleConstructor) {
				fv = cv.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, "_ACCESSOR", PVT_ACC_DESC, null, null);
				fv.visitEnd();

				mv.visitLdcInsn(Type.getType(PrivateAccessor.INSTANCE.getClass()));
				mv.visitFieldInsn(GETSTATIC, Type.getInternalName(PrivateAccessor.class), "INSTANCE", Type.getDescriptor(PrivateAccessor.class));
				mv.visitTypeInsn(CHECKCAST, PVT_ACC_IN);
				mv.visitFieldInsn(PUTSTATIC, fullName, "_ACCESSOR", PVT_ACC_DESC);
			}

			if (ipwf != null) {
				fv = cv.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, "$UPER_SER", gsType.getDescriptor(), null, null);
				fv.visitEnd();

				mv.visitLdcInsn(Type.getType(ipwf));
				mv.visitMethodInsn(INVOKESTATIC, gsfType.getInternalName(), "parentSerializer", "(Ljava/lang/Class;)" + Type.getDescriptor(Serializer.class));
				mv.visitTypeInsn(CHECKCAST, gsType.getInternalName());
				mv.visitFieldInsn(PUTSTATIC, fullName, "$UPER_SER", gsType.getDescriptor());
			}

			final Map<Type, String> cnpd = cachedNonPolyDescs;
			if (cnpd != null) {

				cnpd.remove(rootType);
				for (final Entry<Type, String> e : cnpd.entrySet()) {
					final Type fieldType = e.getKey();

					if (fieldType.equals(rootType)) {

					} else {
						fv = cv.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, e.getValue(), gsType.getDescriptor(), null, null);
						fv.visitEnd();

						mv.visitLdcInsn(fieldType);
						mv.visitMethodInsn(INVOKESTATIC, gsfType.getInternalName(), "alternativeOrDefault", "(Ljava/lang/Class;)" + Type.getDescriptor(Serializer.class));
						mv.visitTypeInsn(CHECKCAST, gsType.getInternalName());
						mv.visitFieldInsn(PUTSTATIC, fullName, e.getValue(), gsType.getDescriptor());
					}
				}
			}

		}

		mv.visitInsn(RETURN);
		mv.visitMaxs(0, 0);
		mv.visitEnd();

		// end static initializer

		// constructor

		mv = cv.visitMethod(ACC_PRIVATE, "<init>", "()V", null, null);
		mv.visitCode();
		mv.visitVarInsn(ALOAD, 0);
		mv.visitMethodInsn(INVOKESPECIAL, gsType.getInternalName(), "<init>", "()V");
		mv.visitInsn(RETURN);
		mv.visitMaxs(0, 0);
		mv.visitEnd();

		// implementations
		mv = new DoReadAdapter(cv.visitMethod(ACC_PUBLIC + ACC_FINAL, "doRead", "(Ljava/nio/ByteBuffer;Ljava/lang/Object;)V", null, null));
		mv.visitCode();
		mv.visitEnd();

		mv = new DoWriteAdapter(cv.visitMethod(ACC_PUBLIC + ACC_FINAL, "doWrite", "(Ljava/nio/ByteBuffer;Ljava/lang/Object;)V", null, null));
		mv.visitCode();
		mv.visitEnd();

		// instatiate
		mv = cv.visitMethod(ACC_PUBLIC, "instantiate", "(Ljava/nio/ByteBuffer;)Ljava/lang/Object;", null, null);
		mv.visitCode();

		if (hasAccessibleConstructor) {
			mv.visitTypeInsn(NEW, rootType.getInternalName());
			mv.visitInsn(DUP);
			mv.visitMethodInsn(INVOKESPECIAL, rootType.getInternalName(), "<init>", "()V");
		} else {
			mv.visitFieldInsn(GETSTATIC, fullName, "_ACCESSOR", PVT_ACC_DESC);
			mv.visitLdcInsn(rootType);
			mv.visitMethodInsn(INVOKEVIRTUAL, PVT_ACC_IN, "allocateInstance", "(Ljava/lang/Class;)Ljava/lang/Object;");
		}

		mv.visitInsn(ARETURN);
		mv.visitMaxs(0, 0);
		mv.visitEnd();

	}

	@Override
	public MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) {

		return null;
	}
}
